/*
//triangle: element_type = 2
//line: element_type = 1
*/

#ifndef GmshMesh_H
#define GmshMesh_H

#include <vector>
#include <string>
#include <iostream>
#include <map>
#include <fstream>
#include <sstream>


namespace gmsh {

    struct PhysicalGroup
    {
        int dim;
        int tag;
        std::string name;
    };


    struct PointEntity
    {
        int tag;
        double x;
        double y;
        double z;
        int number_physical_tag;
        std::vector<int> physical_tag_list;
    };


    struct CurveEntity
    {
        int tag;
        double min_x;
        double min_y;
        double min_z;
        double max_x;
        double max_y;
        double max_z;
        int number_physical_tag;
        std::vector<int> physical_tag_list;
        int number_bounding_point;
        std::vector<int> point_tag_list;

        friend ostream& operator << (ostream& os, CurveEntity& curve_entity)
        {
            os << " " << curve_entity.tag << " " << curve_entity.number_physical_tag;
            for (int physical_tag : curve_entity.physical_tag_list)
            {
                os << " " << physical_tag;
            }
            return os;
        }
    };

    struct SurfaceEntity
    {
        int tag;
        double min_x;
        double min_y;
        double min_z;
        double max_x;
        double max_y;
        double max_z;
        int number_physical_tag;
        std::vector<int> physical_tag_list;
        int number_bounding_curve;
        std::vector<int> curve_tag_list;
    };

    struct VolumeEntity
    {
        int tag;
        double min_x;
        double min_y;
        double min_z;
        double max_x;
        double max_y;
        double max_z;
        int number_physical_tag;
        std::vector<int> physical_tag_list;
        int number_bounding_surface;
        std::vector<int> surface_tag_list;
    };

    struct Node
    {
        int node_tag;
        double x;
        double y;
        double z;

        friend ostream& operator << (ostream& os, Node& node)
        {
            os << " " << node.node_tag << " " << node.x << " " << node.y << " " << node.z;
            return os;
        }
    };

    struct Element
    {
        int element_type;
        int element_tag;
        int entity_tag;
        std::vector<int> node_tag_list;

        friend ostream& operator << (ostream& os, Element& element)
        {
            os << " " << element.element_tag << " " << element.element_type << " " << element.entity_tag;
            for (int node_tag : element.node_tag_list)
            {
                os << " " << node_tag;
            }
            return os;
        }
    };

    class GmshMesh
    {

    public:

        GmshMesh(std::string filename)
        {
            std::fstream fs;
            std::stringstream ss;
            std::string line;

            fs.open(filename, std::ios::in);
            if (!fs)
            {
                //log_error<<"[GmshMesh.h::GmshMesh] unable open gmsh file: "<<filename;
            }

            while (getline(fs, line))
            {
                //read $MeshFormat section
                if (line.find("$MeshFormat") != std::string::npos)
                {
                    getline(fs, line);
                    ss.clear();
                    ss.str(line);

                    double version;
                    int file_type;
                    int data_size;

                    if (ss >> version >> file_type >> data_size)
                    {
                        //log_info<<"[GmshMesh.h::GmshMesh] gmsh version: "<<version;
                    }
                    else
                    {
                        //log_error<<"";
                    }

                }

                //read $PhysicalNames section
                if (line.find("$PhysicalNames") != std::string::npos)
                {
                    getline(fs, line);
                    ss.clear();
                    ss.str(line);

                    int number_physical_name;
                    ss >> number_physical_name;

                    for (int i = 0; i < number_physical_name; i++)
                    {
                        getline(fs, line);
                        ss.clear();
                        ss.str(line);

                        PhysicalGroup physical_group;
                        std::string name_str;
                        ss >> physical_group.dim >> physical_group.tag >> name_str;

                        //remove double quote from name, "left" -> left
                        name_str.erase(remove(name_str.begin(), name_str.end(), '\"'), name_str.end());
                        physical_group.name = name_str;

                        physical_group_map[physical_group.tag] = physical_group;
                    }
                }

                //read $Entities section
                if (line.find("$Entities") != std::string::npos)
                {
                    getline(fs, line);
                    ss.clear();
                    ss.str(line);

                    int number_point_entity;
                    int number_curve_entity;
                    int number_surface_entity;
                    int number_volume_entity;
                    ss >> number_point_entity >> number_curve_entity >> number_surface_entity >> number_volume_entity;

                    for (int i = 0; i < number_point_entity; i++)
                    {
                        getline(fs, line);
                        ss.clear();
                        ss.str(line);

                        PointEntity point_entity;
                        ss >> point_entity.tag >> point_entity.x >> point_entity.y >> point_entity.z;

                        ss >> point_entity.number_physical_tag;
                        for (int j = 0; j < point_entity.number_physical_tag; j++)
                        {
                            int physical_tag;
                            ss >> physical_tag;
                            point_entity.physical_tag_list.push_back(physical_tag);
                        }

                        point_entity_map[point_entity.tag] = point_entity;
                    }

                    for (int i = 0; i < number_curve_entity; i++)
                    {
                        getline(fs, line);
                        ss.clear();
                        ss.str(line);

                        CurveEntity curve_entity;
                        ss >> curve_entity.tag >> curve_entity.min_x >> curve_entity.min_y >> curve_entity.min_z
                            >> curve_entity.max_x >> curve_entity.max_y >> curve_entity.max_z;

                        ss >> curve_entity.number_physical_tag;
                        for (int j = 0; j < curve_entity.number_physical_tag; j++)
                        {
                            int physical_tag;
                            ss >> physical_tag;
                            curve_entity.physical_tag_list.push_back(physical_tag);
                        }

                        ss >> curve_entity.number_bounding_point;
                        for (int j = 0; j < curve_entity.number_bounding_point; j++)
                        {
                            int point_tag;
                            ss >> point_tag;
                            curve_entity.point_tag_list.push_back(point_tag);
                        }

                        curve_entity_map[curve_entity.tag] = curve_entity;
                        //log_info <<"[GmshMesh.h::GmshMesh] curve_entity " << curve_entity;
                    }

                    for (int i = 0; i < number_surface_entity; i++)
                    {
                        getline(fs, line);
                        ss.clear();
                        ss.str(line);

                        SurfaceEntity surface_entity;
                        ss >> surface_entity.tag >> surface_entity.min_x >> surface_entity.min_y >> surface_entity.min_z
                            >> surface_entity.max_x >> surface_entity.max_y >> surface_entity.max_z;

                        ss >> surface_entity.number_physical_tag;
                        for (int j = 0; j < surface_entity.number_physical_tag; j++)
                        {
                            int physical_tag;
                            ss >> physical_tag;
                            surface_entity.physical_tag_list.push_back(physical_tag);
                        }

                        ss >> surface_entity.number_bounding_curve;
                        for (int j = 0; j < surface_entity.number_bounding_curve; j++)
                        {
                            int curve_tag;
                            ss >> curve_tag;
                            surface_entity.curve_tag_list.push_back(curve_tag);
                        }

                        surface_entity_map[surface_entity.tag] = surface_entity;
                    }

                    for (int i = 0; i < number_volume_entity; i++)
                    {
                        getline(fs, line);
                        ss.clear();
                        ss.str(line);

                        VolumeEntity volume_entity;
                        ss >> volume_entity.tag >> volume_entity.min_x >> volume_entity.min_y >> volume_entity.min_z
                            >> volume_entity.max_x >> volume_entity.max_y >> volume_entity.max_z;

                        ss >> volume_entity.number_physical_tag;
                        for (int j = 0; j < volume_entity.number_physical_tag; j++)
                        {
                            int physical_tag;
                            ss >> physical_tag;
                            volume_entity.physical_tag_list.push_back(physical_tag);
                        }

                        ss >> volume_entity.number_bounding_surface;
                        for (int j = 0; j < volume_entity.number_bounding_surface; j++)
                        {
                            int surface_tag;
                            ss >> surface_tag;
                            volume_entity.surface_tag_list.push_back(surface_tag);
                        }

                        volume_entity_map[volume_entity.tag] = volume_entity;
                    }
                }

                //read $Nodes section
                if (line.find("$Nodes") != std::string::npos)
                {
                    getline(fs, line);
                    ss.clear();
                    ss.str(line);

                    int number_entity_block;
                    int number_node;
                    int min_node_tag;
                    int max_node_tag;

                    ss >> number_entity_block >> number_node >> min_node_tag >> max_node_tag;

                    for (int i = 0; i < number_entity_block; i++)
                    {
                        getline(fs, line);
                        ss.clear();
                        ss.str(line);

                        int entity_dim;
                        int entity_tag;
                        int parametric;
                        int number_node_in_block;

                        ss >> entity_dim >> entity_tag >> parametric >> number_node_in_block;

                        std::vector<int> node_tag_list;
                        node_tag_list.resize(number_node_in_block);
                        for (int j = 0; j < number_node_in_block; j++)
                        {
                            getline(fs, line);
                            ss.clear();
                            ss.str(line);

                            ss >> node_tag_list[j];
                        }
                        for (int j = 0; j < number_node_in_block; j++)
                        {
                            getline(fs, line);
                            ss.clear();
                            ss.str(line);

                            Node node;
                            node.node_tag = node_tag_list[j];

                            ss >> node.x >> node.y >> node.z;
                            node_list.push_back(node);

                            //log_info << "[GmshMesh.h::GmshMesh] node: " << node;
                        }
                    }
                }

                //read $Elements section
                if (line.find("$Elements") != std::string::npos)
                {
                    getline(fs, line);
                    ss.clear();
                    ss.str(line);

                    int number_entity_block;
                    int number_element;
                    int min_element_tag;
                    int max_element_tag;

                    ss >> number_entity_block >> number_element >> min_element_tag >> max_element_tag;

                    for (int i = 0; i < number_entity_block; i++)
                    {
                        getline(fs, line);
                        ss.clear();
                        ss.str(line);

                        int entity_dim;
                        int entity_tag;
                        int element_type;
                        int number_element_in_block;

                        ss >> entity_dim >> entity_tag >> element_type >> number_element_in_block;

                        for (int j = 0; j < number_element_in_block; j++)
                        {
                            getline(fs, line);
                            ss.clear();
                            ss.str(line);

                            int element_tag;
                            ss >> element_tag;

                            Element element;

                            //triangle: element_type = 2
                            if (element_type == 2)
                            {
                                element.element_tag = element_tag;
                                element.element_type = element_type;
                                element.entity_tag = entity_tag;

                                element.node_tag_list.resize(3);
                            }
                            //line: element_type = 1
                            else if (element_type == 1)
                            {
                                element.element_tag = element_tag;
                                element.element_type = element_type;
                                element.entity_tag = entity_tag;

                                element.node_tag_list.resize(2);
                            }
                            //tet: element_type = 4
                            else if (element_type == 4)
                            {
                                element.element_tag = element_tag;
                                element.element_type = element_type;
                                element.entity_tag = entity_tag;

                                element.node_tag_list.resize(4);
                            }

                            for (int k = 0; k < element.node_tag_list.size(); k++)
                            {
                                ss >> element.node_tag_list[k];
                            }

                            element_list.push_back(element);

                            //log_info << "[GmshMesh.h::GmshMesh] element: " << element;
                        }
                    }
                }
            }

            fs.close();
            fs.clear();
        };

        ~GmshMesh() {};

        std::map<int, PhysicalGroup> physical_group_map;

        std::map<int, PointEntity> point_entity_map;
        std::map<int, CurveEntity> curve_entity_map;
        std::map<int, SurfaceEntity> surface_entity_map;
        std::map<int, VolumeEntity> volume_entity_map;

        std::vector<Node> node_list;
        std::vector<Element> element_list;
    };

};

#endif